Developer Documentation

Plugin Development Guide

Extend the SuiteQL Query Tool with custom functionality using the plugin architecture

Overview

The SuiteQL Query Tool supports a plugin architecture that allows developers to extend functionality without modifying the main script. Plugins can:

Installation

Plugins are JavaScript files that export a plugin definition object. Files must be named with the .sqt-plugin.js extension (or .sqt-plugin.json for JSON-only plugins).

Setup Steps

  1. 1 Upload plugin files to a folder in your NetSuite File Cabinet
  2. 2 Set CONFIG.PLUGIN_FOLDER_ID in the suitelet to this folder's ID
  3. 3 Refresh the SuiteQL Query Tool

Basic Plugin Structure

(function() {
    return {
        // Required fields
        name: 'my-plugin',
        version: '1.0.0',

        // Optional fields
        minAppVersion: '2026.1',
        description: 'My custom plugin',
        author: 'Your Name',
        dependencies: [],
        disables: [],

        // Plugin functionality
        server: { ... },
        client: { ... },
        ui: { ... },
        settings: { ... }
    };
})()

Plugin Definition

Required Fields

Field Type Description
name string Unique identifier for the plugin
version string Plugin version (e.g., '1.0.0')

Optional Fields

Field Type Description
minAppVersion string Minimum SQT version required
description string Plugin description
author string Plugin author
dependencies array Names of plugins that must load first
disables array Built-in features to hide

Disableable Features

The following feature names can be used in the disables array:

ai - All AI features
ai-chat - AI Chat modal
ai-explain - Explain Query
ai-validate - Validate Query
ai-nlbar - NL bar
export - Export functionality
export-airtable - Airtable export
export-google-sheets - Google Sheets
local-library - Local library
remote-library - Remote library
workbooks - Workbooks
tables-reference - Tables Reference
schema-explorer - Schema Explorer
doc-generator - Doc Generator
share - Share Query
history - Query History
dark-mode - Theme toggle
focus-mode - Focus mode
format - Format Query
options - Options panel

Server-Side Integration

Hooks

Server hooks are called during query execution on the NetSuite server.

server: {
    hooks: {
        // Called before query execution
        // Can modify the query or cancel execution
        onBeforeQuery: function(data, plugin) {
            // data.query - the SQL to execute
            // data.payload - the request payload
            // data.originalQuery - unmodified query
            return data; // Return modified data or undefined
        },

        // Called after successful query
        onAfterQuery: function(data, plugin) {
            // data.query - executed SQL
            // data.payload - request payload
            // data.response - query results
            return data;
        },

        // Called when query execution fails
        onError: function(data, plugin) {
            // data.query - the SQL that failed
            // data.payload - request payload
            // data.error - the error object
        }
    }
}

Custom Handlers

Add custom server-side endpoints:

server: {
    handlers: {
        myHandler: function(context, payload, modules, plugin) {
            // context - NetSuite context object
            // payload - request payload
            // modules - NetSuite modules (file, https, log, etc.)
            // plugin - this plugin's definition

            context.response.write(JSON.stringify({
                result: 'success'
            }));
        }
    }
}
Tip: Call custom handlers from the client using:
fetch(scriptUrl, { body: JSON.stringify({ function: 'plugin_pluginname_myHandler', ... }) })

Client-Side Integration

Hooks

Client hooks are called in the browser.

client: {
    hooks: {
        // Called when app initialization completes
        onInit: function(data) {
            // data.state - app state object
            // data.config - app config
            return data;
        },

        // Called before query execution
        onBeforeQuery: function(data) {
            // data.query - SQL to execute
            // data.options - query options
            // Return { ...data, cancel: true } to cancel
            return data;
        },

        // Called after successful query
        onAfterQuery: function(data) {
            // data.query - executed SQL
            // data.results - query results
            // data.elapsedTime - execution time
            // data.rowCount - number of rows
            return data;
        },

        // Called after results render to DOM
        onResultsDisplay: function(data) {
            // data.data - results data
            // data.viewMode - current view mode
            // data.panel - results panel element
            return data;
        },

        // Called before export
        onBeforeExport: function(data) {
            // data.format - export format
            // data.records - data to export
            // Return { ...data, cancel: true } to cancel
            return data;
        },

        // Called after export
        onAfterExport: function(data) {
            // data.format - export format
            // data.rowCount - rows exported
            return data;
        },

        // Called when editor content changes
        onEditorChange: function(data) {
            // data.content - current editor content
            return data;
        }
    }
}

Initialization Function

The init function runs when the plugin loads and can return a public API:

client: {
    init: function(meta, pluginsApi) {
        // meta - { name, version, description }
        // pluginsApi - plugins object with saveSettings, loadSettings, etc.

        // Return public API (optional)
        return {
            doSomething: function() { ... },
            getStatus: function() { ... }
        };
    }
}
Tip: Access the plugin API from anywhere using:
SQT.plugins.get('my-plugin').doSomething()

UI Injection

Inject HTML at designated points in the interface:

ui: {
    'toolbar-start': '<button onclick="...">Custom</button>',
    'toolbar-end': '<button onclick="...">Custom</button>',
    'more-dropdown': '<div class="sqt-toolbar-dropdown-item">...</div>',
    'ai-dropdown': '<div class="sqt-toolbar-dropdown-item">...</div>',
    'header-right': '<button class="sqt-btn">...</button>',
    'before-editor': '<div class="alert">Notice</div>',
    'editor-toolbar': '<button class="btn btn-sm">...</button>',
    'nl-bar': '<div>NL bar extension</div>',
    'results-header': '<button>Custom action</button>',
    'results-footer': '<div>Summary info</div>',
    'export-menu': '<button onclick="...">Custom Export</button>',
    'options-panel': '<div class="sqt-options-section">...</div>',
    'sidebar-section': '<div>Custom sidebar content</div>',
    'local-library-actions': '<button>...</button>',
    'status-bar': '<span>Status indicator</span>',
    'modals': '<div class="modal">...</div>'
}

Injection Points

Toolbar

PointLocation
toolbar-startAfter Run button
toolbar-endBefore options group
more-dropdownEnd of More dropdown menu
ai-dropdownEnd of AI dropdown menu

Header

PointLocation
header-rightHeader actions area (before sidebar toggle)

Editor

PointLocation
before-editorAbove the editor panel
editor-toolbarEnd of editor mini-toolbar
nl-barAfter natural language bar

Results

PointLocation
results-headerResults header actions area
results-footerBelow results content

Sidebar & Modals

PointLocation
sidebar-sectionEnd of sidebar
export-menuEnd of export modal
local-library-actionsLocal library modal header
modalsAfter all built-in modals

Other

PointLocation
options-panelEnd of options dropdown
status-barStatus bar left section

Dynamic Injection Points

Some injection points (results-header, results-footer) are rendered dynamically when results are displayed. For these, inject via the onResultsDisplay hook:

client: {
    hooks: {
        onResultsDisplay: function(data) {
            // Inject into results header
            var header = document.getElementById('sqtPluginResultsHeader');
            if (header) {
                header.innerHTML = '<button onclick="myAction()">Custom</button>';
            }

            // Inject into results footer
            var footer = document.getElementById('sqtPluginResultsFooter');
            if (footer) {
                footer.innerHTML = '<div>Row count: ' + data.data.rowCount + '</div>';
            }

            return data;
        }
    }
}

Settings

Using the Settings API

// Save settings (localStorage only)
SQT.plugins.saveSettings('my-plugin', { enabled: true });

// Load settings
var settings = SQT.plugins.loadSettings('my-plugin');

// Save to server (persists across browsers)
SQT.plugins.saveSettings('my-plugin', settings, true);

Settings Schema

Define a schema for automatic settings UI generation:

settings: {
    schema: {
        enabled: {
            type: 'boolean',
            default: true,
            label: 'Enable Feature',
            description: 'Toggle this feature on/off'
        },
        threshold: {
            type: 'number',
            default: 100,
            label: 'Threshold',
            description: 'Set the threshold value'
        }
    }
}

Best Practices

  1. Use unique names: Prefix plugin names with your organization (e.g., acme-audit-logger)
  2. Handle errors gracefully: Wrap code in try/catch blocks to prevent breaking the main app
  3. Check for dependencies: Use minAppVersion and dependencies to ensure compatibility
  4. Clean up: Remove event listeners and timers when appropriate
  5. Test thoroughly: Test with and without the plugin enabled
  6. Document your plugin: Include clear usage instructions for other developers

Example Plugin

See query-logger.sqt-plugin.js in the sample-plugins folder for a complete example demonstrating:

Troubleshooting

Plugin not loading

Hooks not firing

UI not appearing

Note: Plugin errors are logged to the browser console with the prefix [SQT Plugins]. Check the console for detailed error messages.